/*
 * boot.s - 被 BIOS 从第0个软盘的引导扇区（第1个扇区）加载到地址 0x7c00 处
 *  然后通过 cs:ip = 0x0000:0x7c00 跳转到本代码执行
 *
 dd if=/dev/zero of=a.img bs=512 count=2880

 as --gstabs --32 -o boot.o boot.s
 ld -m elf_i386 -Ttext 0x0000 boot.o -g -o boot.elf
 objcopy -O binary boot.elf boot.bin

 dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
 *
 */
.global begtext, begdata, begbss, endtext, enddata, endbss, _start
.text
begtext:
.data
begdata:
.bss
begbss:
.text

.equ SYSSIZE, 0x3000    # 内核镜像段长度，换算成字节数要 * 16.也就是 192 KB
.equ SETUPLEN, 4        # setup.s 程序占用的扇区数量
.equ BOOTSEG, 0x07c0    # boot.s 最开始运行的地址
.equ INITSEG, 0x9000    # boot.s 会被自己搬移到此地址段处，并开始执行
.equ SETUPSEG, 0x9020   # setup.s 会被 boot.s 搬移到此处，也就是 boot.s 后的 512 字节处（因为boot.s占用一个扇区，也就是512字节）
.equ SYSSEG, 0x1000     # system（真正的内核） 会被 boot.s 搬移到此段处
.equ ENDSEG, SYSSEG + SYSSIZE  # system 段结束位置

ROOT_DEV:   .byte 0
SWAP_DEV:   .byte 0

.code16
_start:
    /*
        下面这段，是将 ds:si 起始，拷贝 256*2=512 字节到 es:di
        也就是 0x7c0:0x0000 ---> 0x9000:0x0000
     */
    movw $BOOTSEG, %ax
    movw %ax, %ds
    movw $INITSEG, %ax
    movw %ax, %es
    movw $256, %cx
    subw %si, %si
    subw %di, %di
    cld
    rep movsw

    /* 拷贝完成之后，那就跳转到 0x9000:0x0000 处开始执行 */
    ljmp $INITSEG,$go
go:
    /* 跳转之后，重新设置一下各个段寄存器 */
    movw %cs, %ax
    movw $0xfef4, %dx
    movw %ax, %ds
    movw %ax, %es
    movw %ax, %ss       # 设置段寄存器和栈指针
    movw %dx, %sp

    /*
        地址 0x0000:0x0078 是 BIOS 中断向量表（IVT） 中 
        INT 0x1E 的中断处理程序入口地址的存储位置。
		实际它并未存储中断入口函数，而是存储了软盘驱动器参数所在的地址信息 段:偏移
        软盘参数表如下： 偏移 - 字段含义
        0x00 - 步进速率、磁头卸载时间
        0x01 - 磁头加载时间、DMA模式
        0x02 - 马达关闭前的等待时间
        0x03 - 每扇区的字节数， 例如512字节/扇区，该字段的值为 02
        0x04 - 每磁道的扇区
        ......
     */
    push $0
    pop %fs             # 实际相当于 movw $0, %ax; movw %ax, %fs
    movw $0x78, %bx     # fs:bx 是 BIOS 的软盘参数表地址信息

    lgs %fs:(%bx), %si    # 取出软盘参数表的地址，是 段:偏移 = gs:si

    /*
        从 gs:si 读取 12字节 到 es:di
        也就是参数表指向的参数位置的前12字节，放入到 0x9000:0xfef4
     */
    movw %dx, %di
    movw $6, %cx
    cld
    rep movsw %gs:(%si), %es:(%di)

    /*
        已经复制一份软盘参数，现在
        1. 修改每磁道的扇区数量
        2. 让BIOS的中断向量指向新的参数表位置
     */
    movw %dx, %di              # %di = 0xfef4, 是新参数的偏移位置
    movb $18, 4(%di)           # 修改每磁道的扇区数量为 18
    /* 接下来是修改BIOS的磁盘参数表位置，让其指向我们修改后的参数表位置 */
    movw %di, %fs:(%bx)        # [0x0000:0x78] = 0xfef4
    movw %es, %fs:2(%bx)       # [0x0000:0x78 + 2] = 0x9000

    /* 恢复段寄存器 */
    movw %cs, %ax
    movw %ax, %fs
    movw %ax, %gs

    /*
        由于修改了参数表，因此要复位一下，让软盘使用新的参数
        dl=0 表示第 0 个软盘
     */
    xor %ah, %ah
    xor %dl, %dl
    int $0x13

/*
    接下来，需要加载 setup，加载方法是利用 BIOS 的 0x13 中断从软盘0的第2个扇区开始，加载四个扇区，也就是扇区[2, 5]
    BIOS 中断 0x13 的参数说明
        dh - 磁头号；dl - 驱动器号
        ch - 磁道（柱面）号的低8位；cl - 开始扇区(0-5),磁道号高2位(6-7)
        ah - 0x02读磁盘扇区到内存；al - 需要读取的扇区数量
        es:bx 指向数据缓冲区；如果出错则CF置位，错误保存在ah中
    因此执行下面这段代码
    会从软盘0的第2个扇区开始，读取4个扇区，放入到 es:bx = 0x9000:0x0200 位置，也就是物理地址 0x90200 处
    注意，在上面 %es 寄存器已经被设置为 0x9000
 */
load_setup:
    xor %dx, %dx                    # 磁头0，驱动器号0
    movw $0x0002, %cx               # 磁道0，起始扇区2
    movw $0x0200, %bx               # 目的地址偏移 = 0x200, 因此实际起始位置为 0x9000:0x200，对应物理地址为 0x90200
    movw $0x0200, %ax               # 高位 %ah = 0x02 表示读取扇区到内存，al = SETUPLEN = 4 表示读取扇区的数量
    addw $SETUPLEN, %ax
    int $0x13

    /* 如果读取成功，则直接跳到 ok_load_setup 执行 */
    jnc ok_load_setup

    /* 读取失败，则打印错误码和栈指针，复位软盘，尝试重新读取一次 */
    push %ax                        # int 0x13 的错误码保存在寄存器 %ax 中
    call print_nl
    movw %sp, %bp
    call print_hex
    pop %ax

    /* 并尝试复位一下软盘，然后跳到  load_setup 重试 */
    xor %dl, %dl
    xor %ah, %ah
    int $0x13
    jmp load_setup

ok_load_setup:
    /* 
        来到这里表示已经加载 setup 4个扇区成功了
        接下来，获取软盘参数，主要是每磁道最大的扇区数量，存入到 sectors ，方面后续加载 system 需要对多磁道读取时使用
     */
    xor %dl, %dl            # 驱动号是0，表示软盘0
    movb $0x08, %ah         # 功能号 8 表示读取驱动参数
    int $0x13

    /*
        上述中断调用的返回值存放在 cx 寄存器中
        ch - 最大磁道（柱面）号的低8位；cl - 每磁道最大扇区(0-5),最大磁道号高2位(6-7)
	    ch = 0,只保留了 cl，实际就是扇区最大数量，这里是软盘，因此磁道数量为80，cl 的高两位一定是0
     */
    xor %ch, %ch            # 只保留低位 cl，因此 %cx 此时就是 磁道的最大扇区数量，前面实际上已经设置过，就是18
    movw %cx, %cs:sectors   # sectors 存放的是最大扇区数量 [cs:sectors] = cx
    movw $INITSEG, %ax
    movw %ax, %es           # %es = 0x9000

    /* 接下来打印一些信息 */

    /*
        获取光标位置
        功能号：AH = 0x03
        参数：
            BH：页面号
        返回值：
            CH：光标起始行
            CL：光标结束行
            DH：光标所在行
            DL：光标所在列
     */
    movb $0x03, %ah
    xor %bh, %bh
    int $0x10

    /*
        打印字符串
        功能号：ah = 0x13：320×200 256 色图形模式
		es:bp 待打印的字符串地址
		bl 字符属性
		al 光标模式， = 1表示光标移动到显示字符串的尾部
		打印 "Loading"
     */
    movw $9, %cx
    movw $0x0007, %bx
    movw $msg1, %bp
    movw $0x1301, %ax
    int $0x10


/*
    接下来，要开始加载 system 模块了
 */
    movw $SYSSEG, %ax
    movw %ax, %es                   # 将段初始位置定位在 0x1000
    call read_it                    # 调用此函数读取实际数据
    call kill_motor
    call print_nl

/*
    接下来处理根文件系统相关的变量
    检查根设备号是否定义，如果有定义，则直接结束了
	如果没有定义，则根据获取到的最大扇区数量是15还是18来判定是1.2M软盘还是1.44M软盘，对于不同的软盘，赋值不同的设备号，作为根文件系统
 */
    movw %cs:root_dev, %ax
    or %ax, %ax
    jne root_defined
    movw %cs:sectors, %bx
    movw $0x0208, %ax
    cmpw $15, %bx
    je root_defined
    movw $0x021c, %ax
    cmpw $18, %bx
    je root_defined
undef_root:
    jmp undef_root
root_defined:
    movw %ax, %cs:root_dev


    /*
		现在跳到 setup 去执行
		也就是0x9020:0x0000，该命令会导致 cs = 0x9020, ip = 0x0000
	 */
    ljmp $SETUPSEG, $0
/*
    movw $0x0e53, %ax                 # 显示字符 'S'
    int $0x10
loop_forever:
    jmp loop_forever
*/

/*===============================================================================================================*/
/*===============================================================================================================*/


sread:  .word 1+SETUPLEN    # 当前磁道已经读取的扇区数量，在读取 system 之前，已经读取 boot + setup 共五个扇区了
head:   .word 0             # 当前磁头号，0或1
track:  .word 0             # 当前磁道号                  

/*
    读取 system 段，最多读取 192KB 数据
    从软盘的第5个扇区开始加载，如果每个扇区是 512 字节，那么就是 192KB / 512B = 384个扇区，[5, 388]
    从磁头0，磁道0，扇区6开始，把数据加载到 es:bx 指向的段，es:bx的初始值为 0x1000:0x0000，对应物理地址 0x10000
    由于每个段最多只能加载 0x10000 = 64KB 的数据，因此一旦加载 64 KB数据，需要对 段(%es)也进行加 0x1000 (段计算物理地址的时候会 * 16)
 */
read_it:
    movw %es, %ax
    testw $0x0fff, %ax                          # 校验段是不是 64K 对齐，每个段对应的段内偏移是 64K
die:jne die
    xor %bx, %bx                                # %bx 是当前段 已经加载的字节数，一旦超过 64KB，需要切换段
rp_read:
    movw %es, %ax
    cmpw $ENDSEG, %ax                           # 校验段是不是已经来到了限制的最大值
    jb ok1_read                                 # %es < $ENDSEG，则跳转到 ok1_read 继续读
    ret                                         # 读够限制的数量了，直接返回
ok1_read:
    movw %cs:sectors, %ax                       # 读取每个磁道的最大扇区数放入到 %ax
    subw %cs:sread, %ax                         # sread 是当前磁道已经读取的扇区数量，初始值是5，因为已经读取了1个引导扇区 + 4个 setup 扇区
    /*
        此时，%ax 中包含的是当前磁道未读的扇区数量，例如 sectors=18, sread=5, 那么 %ax = 13
        接下来要计算未读取的扇区的总字节数量，并通过当前段已经读的字节数量（还记得吗，每个段最大只能读取64K）
        和未读取的扇区总字节数，来判断当前段还最多能读取多少个扇区
     */
    movw %ax, %cx
    shl $9, %cx                                 # %cx = %ax * 512，未读扇区数量 * 512（每个扇区的字节数），计算未读总字节数
    addw %bx, %cx                               # %bx 中保存的是当前段已经读取的字节数量
                                                # 此时 %cx 包含的是如果把当前磁道的扇区读完，总共的字节数量
    jnc ok2_read                                # 判断 %cx + %bx 是否有进位（有加法溢出），没有进位说明小于 64K，可以把剩余扇区都读完
    je ok2_read                                 # 此时加法有进位（溢出），但是 %cx 如果等于 0，说明刚好能读64K，也直接跳转到 ok2_read 继续读取
    xor %ax, %ax
    subw %bx, %ax                               # %ax = 0 - %bx，实际就是相当于使用 0x10000(64K) - %bx，当前段最多还可以读取的字节数
    shr $9, %ax                                 # %ax = %ax / 512，计算当前段还能读取的扇区数量
ok2_read:
    /*
        调用读取指定磁头，指定磁道的指定扇区数量
        此时，%al = 待读取的扇区数量
     */
    call read_track                             # 读取磁道中的内容
    movw %ax, %cx
    addw sread, %ax                             # ax为当前已经读取扇区数量 + 本次读取扇区数量
    cmpw %cs:sectors, %ax
    jne ok3_read                                # 还没有把本磁道的所有扇区都读完，注意sectors保存的是最大扇区数量，需要继续读，跳到 ok3_read
    movw $1, %ax
    subw head, %ax                              # 本磁道读完了，如果当前磁头已经是1了，说明该柱面的两个磁头对应的磁道都已经读取完毕
    jne ok4_read                                # 如果当前读的是磁头0，且读完了，则还有一个磁头1对应的磁道需要读取，则跳到 ok4_read，此时 ax = 1
    incw track                                  # 磁头0/1对应的两个磁道都读完了，则切换到下一个磁道，此时ax = 0
ok4_read:
    movw %ax, head                              # 此时 %ax 存放的是待读取的磁头号
    xor %ax, %ax
ok3_read:
    movw %ax, sread                             # %ax保存的当前已经读取的扇区数量（已经加上本次读取的扇区数量），如果是新的段，则%ax = 0
    shl $9, %cx                                 # %cx = %cx * 512 = 本次读取的扇区数量 * 512 = 本次读取的总字节数
    addw %cx, %bx                               # 本段已经读取的字节数，需要加上本次读取的字节数
    jnc rp_read                                 # 还没超过段的长度 64 K，则从 rp_read 处开始，继续读取指定 磁头/磁道/扇区 数量
    
    /* 超过段长度了，则需要更新段寄存器 %es */
    movw %es, %ax
    addb $0x10, %ah                             # 高字节 + 0x10，相当于 %ax = %ax + 0x1000
    movw %ax, %es                               # 读取的字节数已经超过一个段的长度了，需要把段的值也往前加
    /* 段计算的时候是*16的，因此es 往前挪动 64K / 16 的位置，也就是0x1000 = 4K，这也说明了为什么 es 要 0xfff对齐的原因 */
    xor %bx, %bx                                # 本段偏移要从0开始
    jmp rp_read

/*
    读取当前磁道中指定的扇区
 */
read_track:
    pusha                           # AX、CX、DX、BX、SP、BP、SI 和 DI 入栈
    
    /* 在 Loading 后继续显示一个字符 . */
    pusha                           # 因为要使用 BIOS 中断，所有再保存一次寄存器，中断结束后读取扇区时候也需要用到
    movw $0xe2e, %ax
    movw $7, %bx
    int $0x10
    popa

    /*
        读取指定磁道中的若干个扇区
        dh - 磁头号；dl - 驱动器号
	    ch - 磁道（柱面）号的低8位；cl - 开始扇区(0-5),磁道号高2位(6-7)
	    ah - 0x02读磁盘扇区到内存；al - 需要读取的扇区数量
	    es:bx 指向数据缓冲区；如果出错则CF置位，错误保存在ah中
     */
    movw track, %dx                 # 获取当前的磁道号
    movw sread, %cx                 # 获取当前已经读取的扇区数量
    inc %cx                         # 已经读取的扇区数量 + 1，也就是下一个待读取的扇区号 -> %cl
    movb %dl, %ch                   # 磁道号低8位保存到 %ch
    movw head, %dx                  # 当前磁头号保存到 %dh
    movb %dl, %dh                   
    andw $0x0100, %dx               # 磁头号dh & 0x01 意味着磁头号不是 0 就是 1， dl = 0意味着是第一个软盘
    movb $2, %ah                    # 在调用本函数前，al中已经是剩余待读取的扇区数量

    /* 保存错误信息需要的寄存器 */
    push %dx
    push %cx
    push %bx
    push %ax

    int $0x13
    jc bad_rt                       # 读取出错，跳转到 bad_rt
    add $8, %sp                     # 上面 4 个push，每个2字节，这里直接 add，相当于4个pop

    popa
    ret
bad_rt:
    push %ax                        # 保存错误码
    call print_all

    /* 尝试重启软盘，并跳转到 read_track 重新读一次 */
    xor %ah, %ah
    xor %dl, %dl
    int $0x13

    addw $10, %sp                   # 跳过保存的错误码和寄存器，共5个参数，10字节
    popa
    jmp read_track                  # 尝试重读


/*
    打印出错时候ax bx cx dx寄存器内容，以及错误码信息
    在调用本函数前，相关寄存器和错误码信息已经压入栈中，分别是
    %dx %cx %bx %ax error_code

    栈分布如下
    dx 
    cx
    bx
    ax
    error
    ret <- sp
 */
print_all:
    movw $5, %cx                    # 5个参数，循环5次
    movw %sp, %bp                   # %bp 指向第一个参数，也就是 返回值的位置
print_loop:
    push %cx                        # 循环次数压栈，避免被破坏
    call print_nl                   # 打印回车换行
    jae no_reg                      # 判断是否需要打印寄存器名字，CF 和 ZF 标志位

    /* 有寄存器，就先打印前缀 AX: BX: 等等 */

    movw $0xe05, %ax
    addw $0x41, %ax
    subw $1, %ax                    # %ax = 0xe05 + 0x41 -1；相当于 %ah = 0x0e(显示字符)，%al = 0x41 - 1 + 5
    subb %cl, %al                   # %al = 0x41 ~ 0x42，依次显示字符 'A' ~ 'D'
    int $0x10

    movb $0x58, %al                 # 显示字符 'X'
    int $0x10

    movb $0x3a, %al                 # 显示字符 ':'
    int $0x10
no_reg:
    add $2, %bp                     # 指向待打印的参数
    call print_hex                  # 以16进制打印寄存器
    pop %cx
    loop print_loop
    ret


/*
	本函数打印换行和回车
 */
print_nl:
	movw $0xe0d, %ax                # CR 回车
	int	$0x10
	movb $0xa, %al                  # LF 换行
	int $0x10
	ret

/*
    循环打印4个字节，以16进制字符形式打印
    bp 指向的位置就是待打印的字
    利用 BIOS 的0x13中段显示字符
        功能号：AH = 0x0E
        参数：
            AL：要显示的字符
            BH：页面号
            BL：字符属性（前景色和背景色）
 */
print_hex:
    movw $4, %cx                # %cx存放的是 loop 次数。2字节的十六进制数，分为4个半字节，每4位打印出一个字符
    movw (%bp), %dx             # 将待打印的十六进制数保存到 %dx
print_digit:
    /* 每4位处理一次，循环左移4位，这样dx最高4位会进入最低4位 */
    rol $4, %dx
    movb $0x0e, %ah             # 功能号

    /*
        al存放待显示的字符，具体逻辑是取 dx 的低 4 位放入到 al 中，并判断数字是否在 0-9 之间(ASCII码)
        如果是的话就直接显示字符 '0'-'9'， al 中存放待显示的字符
        如果数字大于9，那么就将 10-15 数字（4bit最大值为15）换算成字符 A-F
     */
    movb %dl, %al
    andb $0xf, %al                      # 相当于取低4位
    addb $0x30, %al                     # %al = %al + 0x30，相当于将数字转换成对应的 ASCII 码
    cmpb $0x39, %al
    jbe good_digit                      # 如果 %al <= 0x39，那么证明是数字 0-9 转换成的 ASCII 码，直接显示就行
    subb $0x30, %al
    subb $0xa, %al
    addb $0x41, %al                     # %al = $al - 0x30 - 0xa + 0x41，换算成 'A'-'F'
                                        # 例如 %al = 0x3a (即数字10 + 0x30), 则换算后 $al = 0x41 就是字符 'A'
good_digit:
    int $0x10
    loop print_digit
    ret                         # 已经显示4次，则退出循环，返回

kill_motor:
	push %dx
	mov $0x3f2, %dx             # 软盘0的端口
	xor %al, %al
	outb %al, %dx
	pop %dx
	ret

sectors:    .word 0

msg1:
    .byte 13,10                # 13 是回车 CR， 10 是换行符号LF      
	.asciz "Loading"

.org 506
swap_dev:
	.word SWAP_DEV
root_dev:
	.word ROOT_DEV
boot_flag:
	.word 0xAA55

.text
endtext:
.data
enddata:
.bss
endbss:
